﻿(*

Panel Colorer Plugin v 0.94
ConEmu console emulator API bindings

Author: Igor Afanasyev <igor.afanasyev (at) gmail (dot) com>

License: GPL v.3 (see license.txt)

*)

unit ConEmu;

interface

uses
  Windows,
  //Dialogs, {for ShowMessage}
  SysUtils {for Format function};

type
  CESERVER_REQ_HDR = record
    cbSize: DWORD;
    nCmd: DWORD;
    nVersion: DWORD;
    nSrcThreadId: DWORD;
    nSrcPID: DWORD;
    nCreateTick: DWORD;
  end;

  CESERVER_REQ_GETGUIHWND = record
    rootHWND: DWORD;
    childHWND: DWORD;
  end;
  PCESERVER_REQ_GETGUIHWND = ^CESERVER_REQ_GETGUIHWND;

  CESERVER_REQ_SETBACKGROUND = record
    nType: Integer;        // Reserved. Must be 1
    bEnabled: BOOL;        // TRUE - ConEmu use this image, FALSE - ConEmu use self background settings
    nReserved1: Integer;   // Must by 0. reserved for alpha
    nReserved2: DWORD;     // Must by 0. reserved for replaced colors
    nReserved3: Integer;   // Must by 0. reserved for background op
    nReserved4: DWORD;     // Must by 0. reserved for flags (BmpIsTransparent, RectangleSpecified)
    nReserved5: DWORD;     // Must by 0. reserved for level (Some plugins may want to draw small parts over common background)
	  rcReserved5: TRect;    // Must by 0. reserved for filled rect (plugin may cover only one panel, or part of it)
    bmp: BITMAPFILEHEADER; // Bitmap data
  end;
  PCESERVER_REQ_SETBACKGROUND = ^CESERVER_REQ_SETBACKGROUND;

// ConEmu respond for CESERVER_REQ_SETBACKGROUND
  TSetBackgroundResult = (
    esbr_OK,               // All OK
    esbr_InvalidArg,       // Invalid CESERVER_REQ_SETBACKGROUND.bmp, or CESERVER_REQ_SETBACKGROUND
    esbr_PluginForbidden,  // "Allow plugins" unchecked in ConEmu settings ("Main" page)
    esbr_ConEmuInShutdown, // Console is closing. This is not an error, just information
    esbr_Unexpected        // Unexpected error in ConEmu
  );

  CESERVER_REQ_SETBACKGROUNDRET = record
    nResult: Integer; // TSetBackgroundResult
  end;

  CESERVER_REQ = record
    hdr: CESERVER_REQ_HDR;
    dwData: DWORD; // variable length!
  end;
  PCESERVER_REQ = ^CESERVER_REQ;

const
  // from common/common.hpp

  CESERVER_REQ_VER = 48;
  CECMD_GETGUIHWND = 5;
  CECMD_SETBACKGROUND = 34;

  CESERVERPIPENAME = '\\%s\pipe\ConEmuSrv%u';
  CEGUIPIPENAME = '\\%s\pipe\ConEmuGui%u';

  gpNullSecurity : PSecurityAttributes = nil;

function GetConEmuHWND(FarHwnd: HWND; abRoot: Boolean): HWND;
function SetBackground(FarHWND: HWND; Buf: Pointer; BufSize: Int64): TSetBackgroundResult;
function ClearBackground(FarHWND: HWND): TSetBackgroundResult;

implementation

// from common/ConEmuCheck.cpp

function ExecuteOpenPipe(szPipeName: String; var szErr: String; szModule: String): THandle;
var
  hPipe: THandle;
  dwErr, dwMode: DWORD;
  fSuccess: Boolean;
  dwStartTick: DWORD;
  nTries: Integer;
begin
  hPipe := 0;
  dwMode := 0;
  dwStartTick := GetTickCount;
  nTries := 2;

  Result := 0;

  // Try to open a named pipe; wait for it, if necessary.
  while (true) do
  begin
    hPipe := CreateFile(
      PWideChar(szPipeName),     // pipe name
      GENERIC_READ or GENERIC_WRITE,
      0,              // no sharing
      gpNullSecurity, // default security attributes
      OPEN_EXISTING,  // opens existing pipe
      0,              // default attributes
      0);             // no template file

    // Break if the pipe handle is valid.
    if (hPipe <> INVALID_HANDLE_VALUE) then
      Break; // OK, открыли

    dwErr := GetLastError;

    // Сделаем так, чтобы хотя бы пару раз он попробовал повторить
    if ((nTries <= 0) and ((GetTickCount - dwStartTick) > 100)) then
    begin
      if (szModule = '') then szModule := 'Unknown';
      szErr := Format('%s: CreateFile(%s) failed, code=0x%08X, Timeout',
        [szModule, szPipeName, dwErr]);
      Exit;
    end else
    begin
      Dec(nTries);
    end;

    // Может быть пайп еще не создан (в процессе срабатывания семафора)
    if (dwErr = ERROR_FILE_NOT_FOUND) then
    begin
      Sleep(10);
      continue;
    end;

    // Exit if an error other than ERROR_PIPE_BUSY occurs.
    if (dwErr <> ERROR_PIPE_BUSY) then
    begin
      if (szModule = '') then szModule := 'Unknown';
      szErr := Format('%s: CreateFile(%s) failed, code=0x%08X',
        [szModule, szPipeName, dwErr]);
      Exit;
    end;

    // All pipe instances are busy, so wait for 1000 ms.
    if (not WaitNamedPipe(PWideChar(szPipeName), 1000)) then
    begin
      dwErr := GetLastError;
      if (szModule = '') then szModule := 'Unknown';
      szErr := Format('%s: WaitNamedPipe(%s) failed, code=0x%08X, WaitNamedPipe',
        [szModule, szPipeName, dwErr]);
      Exit;
    end;
  end;

  // The pipe connected; change to message-read mode.
  dwMode := PIPE_READMODE_MESSAGE;
  fSuccess := SetNamedPipeHandleState(
      hPipe,  // pipe handle
      dwMode, // new pipe mode
      nil,    // don't set maximum bytes
      nil);   // don't set maximum time

  if (not fSuccess) then
  begin
    dwErr := GetLastError();
    if (szModule = '') then szModule := 'Unknown';
    szErr := Format('%s: SetNamedPipeHandleState(%s) failed, code=0x%08X',
      [szModule, szPipeName, dwErr]);
    CloseHandle(hPipe);
    Exit;
  end;

  Result := hPipe;
end;


//Arguments:
//   hConWnd - Хэндл КОНСОЛЬНОГО окна (по нему формируется имя пайпа для GUI)
//   pIn     - выполняемая команда
//Returns:
//   CESERVER_REQ. Его необходимо освободить через free(...);
//WARNING!!!
//   Эта процедура не может получить с сервера более 600 байт данных!
// В заголовке hOwner в дебаге может быть отображена ошибка
function ExecuteCmd(szGuiPipeName : String; pIn: PCESERVER_REQ; nWaitPipe: DWORD; hOwner: HWND; var szErr: String): PCESERVER_REQ;
var
  pOut: CESERVER_REQ;
	hPipe: THandle;
  cbReadBuf: array[0..600] of Byte; // чтобы CESERVER_REQ_OUTPUTFILE поместился
  fSuccess: Boolean;
  cbRead, dwErr: DWORD;
  nAllSize: Cardinal;
  ptrData: Pointer;
begin
  Result := nil;
  ZeroMemory(@pOut, SizeOf(pOut));
	cbRead := 0;


  if (szGuiPipeName = '') then Exit;
  if (pIn.hdr.nSrcPID = 0) then Exit;
  if (pIn.hdr.nSrcThreadId = 0) then Exit;
  if (pIn.hdr.cbSize < SizeOf(pIn.hdr)) then Exit;
	if (pIn.hdr.nSrcThreadId <> GetCurrentThreadId) then Exit;

	hPipe := ExecuteOpenPipe(szGuiPipeName, szErr, ''(*Сюда хорошо бы имя модуля подкрутить*));
  //if (pIn.hdr.nCmd = CECMD_SETBACKGROUND) then ShowMessageFmt('ExecuteOpenPipe szError = %s', [szErr]);
	if ((hPipe = 0) or (hPipe = INVALID_HANDLE_VALUE)) then
		Exit;

	// Send a message to the pipe server and read the response.
	fSuccess := TransactNamedPipe(
		hPipe,             // pipe handle
		pIn,               // message to server
		pIn.hdr.cbSize,    // message length
		@cbReadBuf,        // buffer to receive reply
		SizeOf(cbReadBuf), // size of read buffer
		&cbRead,           // bytes read
		nil);              // not overlapped

	dwErr := GetLastError;

  (** )
  if (pIn.hdr.nCmd = CECMD_SETBACKGROUND) then
    ShowMessageFmt('szGuiPipeName = %s, SizeOf(cbReadBuf) = %d, dwErr = %d',
                   [szGuiPipeName, SizeOf(cbReadBuf), dwErr]);
  (**)

	if ((not fSuccess) and (dwErr <> ERROR_MORE_DATA)) then
  begin
		CloseHandle(hPipe);
		Exit;
  end;

	if (cbRead < SizeOf(CESERVER_REQ_HDR)) then
  begin
		CloseHandle(hPipe);
		Exit;
  end;

  Move(cbReadBuf, pOut, cbRead); // temporary

	if (pOut.hdr.cbSize < cbRead) then
  begin
		CloseHandle(hPipe);
		szErr := '!!! Wrong nSize received from GUI server !!!';
		Exit;
  end;

	if (pOut.hdr.nVersion <> CESERVER_REQ_VER) then
  begin
		CloseHandle(hPipe);
		szErr := '!!! Wrong nVersion received from GUI server !!!';
		Exit;
  end;

	nAllSize := pOut.hdr.cbSize;
  GetMem(Result, nAllSize);
	if (Result = nil) then
  begin
		CloseHandle(hPipe);
    Exit;
  end;
  Move(cbReadBuf, Result^, cbRead);

	ptrData := Pointer(Cardinal(Result) + cbRead);
	nAllSize := nAllSize - cbRead;

	while (nAllSize > 0) do
  begin
		// Break if TransactNamedPipe or ReadFile is successful
		if (fSuccess) then
			Break;

		// Read from the pipe if there is more data in the message.
		fSuccess := ReadFile(
			hPipe,    // pipe handle
			ptrData,  // buffer to receive reply
			nAllSize, // size of buffer
			cbRead,   // number of bytes read
			nil);       // not overlapped

		// Exit if an error other than ERROR_MORE_DATA occurs.
		if((not fSuccess) and (GetLastError <> ERROR_MORE_DATA)) then
			Break;
		ptrData := Pointer(Cardinal(ptrData) + cbRead);
		nAllSize := nAllSize - cbRead;
  end;

	CloseHandle(hPipe);
end;

// Выполнить в GUI (в CRealConsole)
function ExecuteGuiCmd(hConWnd: HWND; pIn: PCESERVER_REQ; hOwner: HWND; var szErr: String): PCESERVER_REQ;
begin
  Result := nil;
	if (hConWnd = 0) then Exit;

	Result := ExecuteCmd(Format(CEGUIPIPENAME, ['.', hConWnd]), pIn, 1000, hOwner, szErr);
end;

// Выполнить в ConEmuC
function ExecuteSrvCmd(dwSrvPID: DWORD; pIn: PCESERVER_REQ; hOwner: HWND; var szErr: String): PCESERVER_REQ;
begin
  Result := nil;
	if (dwSrvPID = 0) then Exit;

	Result := ExecuteCmd(Format(CESERVERPIPENAME, ['.', dwSrvPID]), pIn, 1000, hOwner, szErr);
end;

procedure ExecutePrepareCmd(pIn: PCESERVER_REQ; nCmd, cbSize: DWORD);
begin
  pIn.hdr.nCmd := nCmd;
	pIn.hdr.nSrcThreadId := GetCurrentThreadId;
	pIn.hdr.nSrcPID := GetCurrentProcessId;
	pIn.hdr.cbSize := cbSize;
	pIn.hdr.nVersion := CESERVER_REQ_VER;
	pIn.hdr.nCreateTick := GetTickCount;
end;

procedure ExecuteFreeResult(pOut: PCESERVER_REQ);
begin
  if (pOut = nil) then Exit;
  FreeMem(pOut, pOut.hdr.cbSize);
end;

// Returns HWND of Gui console DC window
function GetConEmuHWND(FarHWND: HWND; abRoot: Boolean): HWND;
var
  pIn: PCESERVER_REQ;
  pOut: PCESERVER_REQ;
  reqSize: Cardinal;
  szErr: String;
begin
  Result := 0;

  if (FarHWND = 0) then Exit;

  reqSize := SizeOf(CESERVER_REQ_HDR) + SizeOf(CESERVER_REQ_GETGUIHWND);
  GetMem(pIn, reqSize);
  try
    ZeroMemory(pIn, reqSize);

    ExecutePrepareCmd(pIn, CECMD_GETGUIHWND, reqSize);

    pOut := ExecuteGuiCmd(FarHWND, pIn, 0, szErr);
    if (pOut = nil) then
      Exit;

    if ((pOut.hdr.cbSize <> reqSize) or
        (pOut.hdr.nCmd <> pIn.hdr.nCmd)) then
    begin
      ExecuteFreeResult(pOut);
      Exit;
    end;

    if (abRoot) then
      Result := PCESERVER_REQ_GETGUIHWND(Addr(pOut.dwData)).rootHWND
    else
      Result := PCESERVER_REQ_GETGUIHWND(Addr(pOut.dwData)).childHWND;

    ExecuteFreeResult(pOut);
  finally
    FreeMem(pIn, reqSize);
  end;
end;

// Transfers BMP background to ConEmu
function SetBackground(FarHWND: HWND; Buf: Pointer; BufSize: Int64): TSetBackgroundResult;
var
  pIn: PCESERVER_REQ;
  pOut: PCESERVER_REQ;
  reqSize: Integer;
  szErr: String;
begin
  Result := esbr_OK;

  if (FarHWND = 0) then Exit;

  reqSize := SizeOf(CESERVER_REQ_HDR)
             + SizeOf(CESERVER_REQ_SETBACKGROUND) - SizeOf(BITMAPFILEHEADER) // size of header with no bitmap data
             + BufSize;
  GetMem(pIn, reqSize);
  try
    ZeroMemory(pIn, reqSize);

    PCESERVER_REQ_SETBACKGROUND(@PIn.dwData).nType := 1;
    PCESERVER_REQ_SETBACKGROUND(@PIn.dwData).bEnabled := (Buf <> nil);
    Move(Buf^, PCESERVER_REQ_SETBACKGROUND(@PIn.dwData).bmp, BufSize);

    ExecutePrepareCmd(pIn, CECMD_SETBACKGROUND, reqSize);
    pOut := ExecuteGuiCmd(FarHWND, pIn, FarHWND, szErr);
    if (pOut = nil) then
      Exit;

    Result := TSetBackgroundResult(CESERVER_REQ_SETBACKGROUNDRET(pOut.dwData).nResult);

    ExecuteFreeResult(pOut);
  finally
    FreeMem(pIn, reqSize);
  end;
end;

// Clears ConEmu background
function ClearBackground(FarHWND: HWND): TSetBackgroundResult;
begin
  Result := SetBackground(FarHWND, nil, 0);
end;

end.
